package org.acegisecurity.util.expressioninterpreter;

import java.util.StringTokenizer;

import org.acegisecurity.Authentication;
import org.acegisecurity.util.ExpressionEvaluator;
import org.acegisecurity.util.IllegalExpressionException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Interprets a given logical expression and checks if the given principal has
 * all the roles required which are given by the logical expression.
 *
 * @author Jamie Craane
 * @author Ivo Woltring
 */
public class ExpressionInterpreter implements ExpressionEvaluator {
    private static Log log = LogFactory.getLog(ExpressionInterpreter.class);
    private static final String RIGHT_PARENTHESIS_OPERATOR = ")";

    /**
     * Matches a given logical expression against the given authentication and
     * checks if the given authentication has the required roles.
     *
     * @param expr           The logical expression to parse
     * @param authentication The authentication which has roles to check the logical
     *                       expression against return true if the given authentication has
     *                       the required roles specified by the logical expression, false
     *                       otherwise
     */
    public boolean match(String expr, Authentication authentication) throws IllegalExpressionException {
        String[] roles = getRoles(authentication);
        Stack valueStack = new Stack();
        Stack operatorStack = new Stack();
        Tokenizer tokenizer = new Tokenizer(expr);

        boolean accessGranted = false;
        try {
            while (tokenizer.hasMoreTokens()) {
                String token = tokenizer.nextToken();

                if (OrOperator.OR_OPERATOR.equalsIgnoreCase(token)) {
                    Operator thisOp = new OrOperator();
                    while (!operatorStack.empty() && ((Operator) operatorStack.peek())
                            .hasGreaterPrecedenceThan(thisOp)) {
                        accessGranted = ((Operator) operatorStack.pop()).apply(roles, valueStack);
                    }
                    operatorStack.push(thisOp);
                } else if (AndOperator.AND_OPERATOR.equalsIgnoreCase(token)) {
                    Operator thisOp = new AndOperator();
                    while (!operatorStack.empty() && ((Operator) operatorStack.peek())
                            .hasGreaterPrecedenceThan(thisOp)) {
                        accessGranted = ((Operator) operatorStack.pop()).apply(roles, valueStack);
                    }
                    operatorStack.push(thisOp);
                } else if (NotOperator.NOT_OPERATOR.equalsIgnoreCase(token)) {
                    Operator thisOp = new NotOperator();
                    while (!operatorStack.empty() && ((Operator) operatorStack.peek())
                            .hasGreaterPrecedenceThan(thisOp)) {
                        accessGranted = ((Operator) operatorStack.pop()).apply(roles, valueStack);
                    }
                    operatorStack.push(thisOp);
                } else if (LeftParenthesisOperator.LEFT_PARENTHESIS_OPERATOR.equalsIgnoreCase(token)) {
                    operatorStack.push(new LeftParenthesisOperator());
                } else if (RIGHT_PARENTHESIS_OPERATOR.equalsIgnoreCase(token)) {
                    while (!(operatorStack.peek() instanceof LeftParenthesisOperator)) {
                        accessGranted = ((Operator) operatorStack.pop()).apply(roles, valueStack);
                    }
                    // Don't forget to remove the left parenthesis from the operator stack
                    operatorStack.pop();
                } else {
                    valueStack.push(token);
                }
            }

            // Apply the remaining operators on the operator stack
            while (!operatorStack.empty()) {
                accessGranted = ((Operator) operatorStack.pop()).apply(roles, valueStack);
            }
        } catch (Exception e) {
            log.error("An error occured during evaluation of the expression [" + expr + "]", e);
            throw new IllegalExpressionException(e);
        }

        return accessGranted;
    }

    /**
     * Returns the roles for the authentication as a String[]
     *
     * @param authentication The authentication to extract the roles from.
     * @return The roles as a String[]
     */
    private static String[] getRoles(Authentication authentication) {
        String[] roles = new String[authentication.getAuthorities().length];
        for (int i = 0; i < authentication.getAuthorities().length; i++) {
            roles[i] = authentication.getAuthorities()[i].getAuthority();
        }
        return roles;
    }

    /**
     * This Tokenizer class is a helper class around the StringTokenizer. It
     * instantiates a StringTokenizer with the needed delimiters and tells the
     * StringTokenizer to return the delimiters as tokens. We need the
     * delimiters as tokens to because otherwize it is not possible to determine
     * which expression belong to which part in the expression whitespace is
     * eten by this tokenizer
     *
     * @author Woltring
     * @author Jamie Craane
     */
    static class Tokenizer extends StringTokenizer {
        private static final boolean RETURN_DELIMS_YES = true;
        private static final int NOT_A_TOKEN = 0;

        Tokenizer(String expression) {
            super(expression, " ()", RETURN_DELIMS_YES);
        }

        public String nextToken() {
            while (super.hasMoreTokens()) {
                String token = super.nextToken().trim();
                if (token.length() > NOT_A_TOKEN) {
                    return token;
                }
            }
            return null;
        }
    }
}
